/*
 * Copyright (c) 2014-2020 Bjoern Kimminich.
 * SPDX-License-Identifier: MIT
 */

const insecurity = require('../lib/insecurity')
const models = require('../models/')
const otplib = require('otplib')
const utils = require('../lib/utils')
const challenges = require('../data/datacache').challenges
const config = require('config')

otplib.authenticator.options = {
  // Accepts tokens as valid even when they are 30sec to old or to new
  // This is a standard as the clocks of the authenticator and server might not align perfectly.
  window: 1
}

async function verify (req, res) {
  const { tmpToken, totpToken } = req.body

  try {
    const { userId, type } = insecurity.verify(tmpToken) && insecurity.decode(tmpToken)

    if (type !== 'password_valid_needs_second_factor_token') {
      throw new Error('Invalid token type')
    }

    const user = await models.User.findByPk(userId)

    const isValid = otplib.authenticator.check(totpToken, user.totpSecret)

    const plainUser = utils.queryResultToJson(user)

    if (!isValid) {
      return res.status(401).send()
    }
    utils.solveIf(challenges.twoFactorAuthUnsafeSecretStorageChallenge, () => { return user.email === 'wurstbrot@' + config.get('application.domain') })

    const [basket] = await models.Basket.findOrCreate({ where: { userId }, defaults: {} })

    const token = insecurity.authorize(plainUser)
    plainUser.bid = basket.id // keep track of original basket for challenge solution check
    insecurity.authenticatedUsers.put(token, plainUser)

    res.json({ authentication: { token, bid: basket.id, umail: user.email } })
  } catch (error) {
    res.status(401).send()
  }
}

/**
 * Check the 2FA status of the currently signed in user.
 *
 * When 2FA isnt setup, the result will include data required to start the setup.
 */
async function status (req, res) {
  try {
    const data = insecurity.authenticatedUsers.from(req)
    if (!data) {
      throw new Error('You need to be logged in to see this')
    }
    const { data: user } = data

    if (user.totpSecret === '') {
      const secret = await otplib.authenticator.generateSecret()

      res.json({
        setup: false,
        secret,
        email: user.email,
        setupToken: insecurity.authorize({
          secret,
          type: 'totp_setup_secret'
        })
      })
    } else {
      res.json({
        setup: true
      })
    }
  } catch (error) {
    res.status(401).send()
  }
}

/**
 * Sets Up 2FA for a User
 * Requires 3 params:
 * 1. The Users Password as a confirmation.
 * 2. A Setup token. This is returned by the status endpoint.
 *    This contains a signed TOTP secret to ensure that the secret
 *    was generated by the server and wasn't tampered with by the client
 * 3. The first TOTP Token, generated by the TOTP App. (e.g. Google Authenticator)
 */
async function setup (req, res) {
  try {
    const data = insecurity.authenticatedUsers.from(req)
    if (!data) {
      throw new Error('Need to login before setting up 2FA')
    }
    const { data: user } = data

    const { password, setupToken, initialToken } = req.body

    if (user.password !== insecurity.hash(password)) {
      throw new Error('Password doesnt match stored password')
    }

    if (user.totpSecret !== '') {
      throw new Error('User has 2fa already setup')
    }

    const { secret, type } = insecurity.verify(setupToken) && insecurity.decode(setupToken)
    if (type !== 'totp_setup_secret') {
      throw new Error('SetupToken is of wrong type')
    }
    if (!otplib.authenticator.check(initialToken, secret)) {
      throw new Error('Initial token doesnt match the secret from the setupToken')
    }

    // Update db model and cached object
    const userModel = await models.User.findByPk(user.id)
    userModel.totpSecret = secret
    await userModel.save()
    insecurity.authenticatedUsers.updateFrom(req, utils.queryResultToJson(userModel))

    res.status(200).send()
  } catch (error) {
    res.status(401).send()
  }
}

/**
 * Disables 2fa for the current user
 */
async function disable (req, res) {
  try {
    const data = insecurity.authenticatedUsers.from(req)
    if (!data) {
      throw new Error('Need to login before setting up 2FA')
    }
    const { data: user } = data

    const { password } = req.body

    if (user.password !== insecurity.hash(password)) {
      throw new Error('Password doesnt match stored password')
    }

    // Update db model and cached object
    const userModel = await models.User.findByPk(user.id)
    userModel.totpSecret = ''
    await userModel.save()
    insecurity.authenticatedUsers.updateFrom(req, utils.queryResultToJson(userModel))

    res.status(200).send()
  } catch (error) {
    res.status(401).send()
  }
}

module.exports.disable = () => disable
module.exports.verify = () => verify
module.exports.status = () => status
module.exports.setup = () => setup
